// Ofri Keidar 302933981 group 05

package view;

import controller.ClientController;
import controller.ServerException;

import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;

/**
 * This class implements View interface. The class implements communication with user via console.
 * User is asked for wanted options and the class uses the controller to perform the tasks
 */
public class UserInterface implements View {

	// names of Building element's tags
	private static final String BUILDING_TAG = "Building";	
	private static final String STREET_TAG = "street";
	private static final String BUILDING_NUMBER_TAG = "buildingNumber";
	private static final String APARTMENTS_TAG = "Apartments";
	
	enum SysModes{ADD_BLD, SHOW_INFO, UPDATE_RSDNT, SHUT_DOWN} // holds system modes
	enum Queries{GET_VACANT_FLATS, GET_VACANT_FLATS_BY_ROOMS, GET_VACANT_FLATS_BY_ROOMS_AND_ADRS, GET_FLATS_BY_ADRS,
		GET_ALL_FLATS, GET_ALL_PENTHOUSES, GET_ALL_GARDEN_APRTMNTS, GET_NUM_OF_VACANT_APRTS, GET_NUM_OF_VACANT_APRTS_BY_ADRS} // holds queries that can be done
	private ClientController transactor; // used to perform the tasks

	/**
	 * Prints main menu (modes) to the screen, so the user can choose wanted mode
	 */
	private void showMainMenu() {
		
		// print modes and their numeric values
		System.out.println("Please choose one of the following:");
		System.out.println((SysModes.ADD_BLD.ordinal()+1) + ". Add a new building");
		System.out.println((SysModes.SHOW_INFO.ordinal()+1) + ". Show info");
		System.out.println((SysModes.UPDATE_RSDNT.ordinal()+1) + ". Update resident's details");
		System.out.println((SysModes.SHUT_DOWN.ordinal()+1) + ". Shut down");
	}
	
	/**
	 * Prints queries to the screen, so the user can choose wanted query to database
	 */
	private void showQueries() {
		
		// print queries and their numeric values
		System.out.println("Please insert query number:");
		System.out.println((Queries.GET_VACANT_FLATS.ordinal()+1) + ". Show details of all vacant flats in system");
		System.out.println((Queries.GET_VACANT_FLATS_BY_ROOMS.ordinal()+1) + ". Show details of all vacant flats with certain room number");
		System.out.println((Queries.GET_VACANT_FLATS_BY_ROOMS_AND_ADRS.ordinal()+1) + ". Show details of all vacant flats with certain room number at certain address");
		System.out.println((Queries.GET_FLATS_BY_ADRS.ordinal()+1) + ". Show details of all flats at certain address");
		System.out.println((Queries.GET_ALL_FLATS.ordinal()+1) + ". Show details of all flats in system");
		System.out.println((Queries.GET_ALL_PENTHOUSES.ordinal()+1) + ". Show details of all penthouses at certain address");
		System.out.println((Queries.GET_ALL_GARDEN_APRTMNTS.ordinal()+1) + ". Show details of all garden apartments at certain address");
		System.out.println((Queries.GET_NUM_OF_VACANT_APRTS.ordinal()+1) + ". Find how many vacant apartments are in system");
		System.out.println((Queries.GET_NUM_OF_VACANT_APRTS_BY_ADRS.ordinal()+1) + ". Find how many vacant apartments are at certain address");
	}
	
	/**
	 * Asks user for XML file that holds the buildings details.
	 * Extracts the parameters for each building in file, and sends
	 * the parameters to the controller in order to create buildings according
	 * to the parameters.
	 * If had errors reading the XML file or parameters are of incorrect format, 
	 * displays error message.
	 */
	private void addBuilding() {
		
		try {
			
			// ask for XML file name
			String fileName = Console.readString("Please enter XML file name\n");
			
			// build the document
			SAXBuilder builder = new SAXBuilder();
			Document doc;
			doc = builder.build(fileName);
			
			// create buildings according to given file
			boolean couldParse = parseBuildingsFromXML(doc);
						
			// check if had errors in parsing
			if (!couldParse) {
				System.out.println("Error- could not create buildings according to given XML file!");
			}			
			
		} catch (JDOMException e) {

			// notify error
			System.out.println("Error- could not create buildings according to given XML file!");
			
		} catch (IOException e) {

			// notify error
			System.out.println("Error- could not create buildings according to given XML file!");
		}
	}
	
	/**
	 * Extracts buildings parameters from given XML file and uses the controller
	 * to create buildings according to these paremeters
	 * @param buildingsDocument- XML file holding building's parameters
	 * @return True if could add buildings according to given XML file,
	 * 		   False if not
	 */
	@SuppressWarnings("rawtypes")
	public boolean parseBuildingsFromXML(Document buildingsDocument) {
		
		// get document's root element
		Element root = buildingsDocument.getRootElement();			
		
		// get list of all building elements
		List buildings = root.getChildren(BUILDING_TAG);
		
		// scan all building elements and parse all buildings
		int numOfBuildings = buildings.size();
		
		// check if there are any buildings in the document
		if (numOfBuildings == 0) {
			
			// no buildings in document
			return false;
		}
		
		for (int i = 0 ; i < numOfBuildings ; ++i) {
			
			// get current building element
			Element currBuilding = (Element) buildings.get(i);
			
			// check if street name tag exists
			if (currBuilding.getChild(STREET_TAG) == null) {
				
				// no street name- cannot parse building
				return false;
			}

			// get current building's address
			String stName = currBuilding.getChild(STREET_TAG).getValue();
			
			// check if street name is given
			if (stName.equals("")) {
				
				// no street name- cannot parse building
				return false;
			}
			
			// check if building number tag exists
			if (currBuilding.getChild(BUILDING_NUMBER_TAG) == null) {
				
				// no building number- cannot parse building
				return false;
			}
			
			// get current building's number
			int bldNumber = 0;
			try {
				bldNumber = Integer.parseInt(currBuilding.getChild(BUILDING_NUMBER_TAG).getValue());
			} catch (NumberFormatException e) {
				// building's number tag's content is not a numeric value
				return false;
			}
							
			// extract current building's apartments
			List< Map<String, String> > apartmentsParameters = new LinkedList<Map<String,String>>();
			extractBuildingParameters(currBuilding, apartmentsParameters);
			
			// check file's content correctness
			if (apartmentsParameters.isEmpty()) {
				
				// current building's data could not be parsed
				return false;
			}				
							
			// create building according to parsed data
			try {
				
				// check if could add current building
				boolean couldAddBld = transactor.addBuilding(apartmentsParameters, stName, bldNumber);				
				if (!couldAddBld) {
					
					// could not add building
					return false;
				}
				
			} catch (ServerException e) {
				
				// could not add building
				return false;
			}
		}
		
		// data has been successfully parsed
		return true;
	}

	/**
	 * Extracts parameters for each apartment of given building element.
	 * Each apartment's parameters are stored in a map, and the maps of given building's apartments
	 * are stored in a list
	 * @param building- building element to be parsed 
	 * @param apartmentsParameters- output parameter to hold the parsed parameters
	 */
	@SuppressWarnings("rawtypes")
	private void extractBuildingParameters(Element building, List< Map<String, String> > apartmentsParameters) {
		
		// get the node that holds the apartments
		Element apartmentsNode = building.getChild(APARTMENTS_TAG);
		
		// check that the node exists
		if (apartmentsNode == null) {
			
			// no apartments in building
			apartmentsParameters.clear();
			return;
		}
		
		// get the apartments 
		List apartments = apartmentsNode.getChildren();
		
		// scan the apartment elements and parse their parameters according to their type
		int numOfApartments = apartments.size(); 
		for (int i = 0 ; i < numOfApartments ; ++i) {			

			// parameters map of current apartment
			Map<String, String> currApartmentParamterMap = new HashMap<String, String>();
			
			// add current apartment's type
			currApartmentParamterMap.put(ApartmentDetails.TYPE, ((Element) apartments.get(i)).getName());
			
			// current apartment's parameters
			List currApartmentParameters = ((Element) apartments.get(i)).getChildren();
			
			// get current apartment's parameters
			int numOfParameters = currApartmentParameters.size();
			for (int j = 0 ; j < numOfParameters ; ++j) {
				
				// store current parameter with its tag name
				currApartmentParamterMap.put( ((Element)currApartmentParameters.get(j)).getName(), ((Element)currApartmentParameters.get(j)).getValue());
			}
			
			// store current apartment's parameters with building's other apartments' parameters
			apartmentsParameters.add(currApartmentParamterMap);
		}
		
		// done extracting apartments details
		return;
	}
	
	/**
	 * Gets a list of apartments and prints each apartment's details according to the following format:
	 * address, apartment number, type, number of rooms and total area
	 * @param apartmentsMap- apartments map to print
	 */
	private void printApartments(List< Map<String, String> > apartmentsToPrint) {
		
		// scan given map and print each apartment
		int numOfApartments = apartmentsToPrint.size();
		for (int i = 0 ; i < numOfApartments ; ++i) {
			
			// get parameter map of current apartment
			Map<String, String> currApartmentParams = apartmentsToPrint.get(i);

			// initialize string buffer to hold current aprtment's details
			StringBuffer currApartmentDetails = new StringBuffer("");
			
			// add address
			currApartmentDetails.append("Address: " + currApartmentParams.get(ApartmentDetails.ADDRESS));
			
			// add apartment number
			currApartmentDetails.append(", Apartment #: " + currApartmentParams.get(ApartmentDetails.APARTMENT_NUMBER));
			
			// add type
			currApartmentDetails.append(", Type: " + currApartmentParams.get(ApartmentDetails.TYPE));
			
			// add number of rooms
			currApartmentDetails.append(", Room #: " + currApartmentParams.get(ApartmentDetails.ROOM_NUMBER));
			
			// add total area
			currApartmentDetails.append(", Total area: " + currApartmentParams.get(ApartmentDetails.TOTAL_AREA));
			
			// print current apartment's details
			System.out.println(currApartmentDetails);
		}
	}
	
	/**
	 * Shows user queries menu and asks the controller object to carry out given query
	 */
	private void runQuery() {

		// holds numeric value of query
		int query;
		
		// keep asking user for query, stop asking when given value is valid
		while (true) {
			
			// show possible queries to user
			showQueries(); 
			// get user's choice
			query = Console.readInt(); 
			// check if given option is a possible value
			if (query < 0 || Queries.values().length < query) {
				// invalid option
				System.out.println("Invalid option, please try again.");
			} else {
				// valid option is given, stop looping
				break;				
			}
		}
		
		// find which option user has chosen
		Queries choise = Queries.values()[query-1];
		
		// perform task given by user
		switch (choise) {
		
		case GET_VACANT_FLATS: {
			
			try {
			
				// get all vacant flats in system
				List< Map<String, String> > vacantFlats = transactor.getVacantFlats();
			
				// if there are not any, notify user
				if (vacantFlats.isEmpty()) {
					System.out.println("No vacant flats in system");
					break;
				}
				
				// flats were returned, scan given list and print each flat
				printApartments(vacantFlats);
				
			} catch (ServerException e) {
				
				// display error message to user
				System.out.println(e.getMessage());
			}
			
			break;
		}
		
		case GET_VACANT_FLATS_BY_ROOMS: {
			
			// ask user for room number to search
			double roomNumber = Console.readDouble("Please insert room number to search:\n");
			
			try {
			
				// get all vacant flats with given room number in system
				List< Map<String, String> > vacantFlats = transactor.getVacantFlats(roomNumber);
				
				// if there are not any, notify user
				if (vacantFlats.isEmpty()) {
					System.out.println("No vacant flats with " + roomNumber + " rooms in system");
					break;
				}
				
				// flats were returned, scan given list and print each flat
				printApartments(vacantFlats);
				
			} catch (ServerException e) {
				
				// display error message to user
				System.out.println(e.getMessage());
			}
			
			break;
		}
		
		case GET_VACANT_FLATS_BY_ROOMS_AND_ADRS: {
			
			// ask user for room number and address to search
			double roomNumber = Console.readDouble("Please insert room number to search:\n");
			String stName = Console.readString("Please insert street name:\n");
			int bldNumber = Console.readInt("Please insert building number:\n");
			
			try {
			
				// get all vacant flats with given room number at given address
				List< Map<String, String> > vacantFlats = transactor.getVacantFlats(roomNumber, stName, bldNumber);
				
				// if there are not any, notify user
				if (vacantFlats.isEmpty()) {
					System.out.println("No vacant flats with " + roomNumber + " rooms at " + bldNumber + " " + stName);
					break;
				}
				
				// flats were returned, scan given list and print each flat
				printApartments(vacantFlats);
				
			} catch (ServerException e) {
				
				// display error message to user
				System.out.println(e.getMessage());
			}
			
			break;
		}
		
		case GET_FLATS_BY_ADRS: {
			
			// ask user for address to search
			String stName = Console.readString("Please insert street name:\n");
			int bldNumber = Console.readInt("Please insert building number:\n");
			
			try {
			
				// get all flats at given address
				List< Map<String,String> > flats = transactor.getFlats(stName, bldNumber);
				
				// if there are not any, notify user
				if (flats.isEmpty()) {
					System.out.println("No flats at " + bldNumber + " " + stName);
					break;
				}
				
				// flats were returned, scan given list and print each flat
				printApartments(flats);
				
			} catch (ServerException e) {
				
				// display error message to user
				System.out.println(e.getMessage());
			}
			
			break;
		}
			
		case GET_ALL_FLATS: {
			
			try {
			
				// get all flats in system
				List< Map<String,String> > flats = transactor.getFlats();
				
				// if there are not any, notify user
				if (flats.isEmpty()) {
					System.out.println("No flats in system");
					break;
				}
				
				// flats were returned, scan given list and print each flat
				printApartments(flats);
				
			} catch (ServerException e) {
				
				// display error message to user
				System.out.println(e.getMessage());
			}
				
			break;
		}
			
		case GET_ALL_PENTHOUSES: {
			
			try {
			
				// get all penthouses in system
				List< Map<String, String> > penthouses = transactor.getPenthouses();
				
				// if there are not any, notify user
				if (penthouses.isEmpty()) {
					System.out.println("No penthouses in system");
					break;
				}
				
				// penthouses were returned, scan given list and print each penthouse
				printApartments(penthouses);
				
			} catch (ServerException e) {
				
				// display error message to user
				System.out.println(e.getMessage());
			}
				
			break;
		}
			
		case GET_ALL_GARDEN_APRTMNTS: {
			
			try {
			
				// get all garden apartments in system
				List< Map<String,String> > gardenAprts = transactor.getGardenApartments();
				
				// if there are not any, notify user
				if (gardenAprts.isEmpty()) {
					System.out.println("No garden apartments in system");
					break;
				}
				
				// garden apartments were returned, scan given list and print each garden apartment
				printApartments(gardenAprts);
				
			} catch (ServerException e) {
				
				// display error message to user
				System.out.println(e.getMessage());
			}
			
			break;
		}
			
		case GET_NUM_OF_VACANT_APRTS: {
			
			try {
			
				// print how many vacant apartments there are in entire system			
				System.out.println("Number of vacant apartments is system: " + transactor.getVacantAprtsNum());
			
			} catch (ServerException e) {
				
				// display error message to user
				System.out.println(e.getMessage());
			}
			
			break;
		}
			
		case GET_NUM_OF_VACANT_APRTS_BY_ADRS: {
			
			// ask user for building's address 
			String stName = Console.readString("Please insert street name:\n");
			int bldNumber = Console.readInt("Please insert building number:\n");
			
			try {
			
				// print how many vacant apartments there are in entire system			
				System.out.println("Number of vacant apartments at " + bldNumber + " " + stName + ": " + transactor.getVacantAprtsNum(stName, bldNumber));
			
			} catch (ServerException e) {
				
				// display error message to user
				System.out.println(e.getMessage());
			}
			
			break;
		}
		
		}
	}
	
	/**
	 * Asks user for address, apartment number and new resident name.
	 * If building address or apartment numbers are wrong, or the apartment matching
	 * given details is occupied, resident is not updated.
	 * Otherwise, sets given name as the wanted apartment's resident name
	 */
	private void updateResident() {
		
		// ask user for address and apartment number 
		String stName = Console.readString("Please insert street name:\n");
		int bldNum = Console.readInt("Please insert building number:\n");
		int apartmentNum = Console.readInt("Please insert apartment number:\n");
		// ask user for new resident name
		String newResident = Console.readString("Please insert new resident name:\n");
		
		try {
		
			// try updating resident
			boolean couldUpdate = transactor.updateResident(stName, bldNum, apartmentNum, newResident);
			if (!couldUpdate) {
				System.out.println("Could not update resident- wrong apartment details or occupied apartment.");
			}
		
		} catch (ServerException e) {
			
			// display error message to user
			System.out.println(e.getMessage());
		}
	}
	
	/**
	 * Constructor, initializes a new user-interface object
	 * @param transactor- controller object to be used
	 * @throws DBException- throws database exception if could not initialize system
	 */
	public UserInterface(ClientController transactor) {

		// set controller 
		this.transactor = transactor;
	}
	
	/**
	 * Prints options for user and performs user's choices. 
	 * When user wishes to exit program, data is saved.
	 */
	public void startRunning() {

		int option; // hold user chosen option
		boolean keepRunning = true; // indicates if should keep asking user for options
		
		// keep looping and asking user for options
		while (keepRunning) {
			
			// keep looping until option given by user is valid
			while (true) {
				
				// show main menu
				showMainMenu(); 
				// get user's choice
				option = Console.readInt(); 
				// check if given option is a possible value				
				if (option < 0 || SysModes.values().length < option) {
					// invalid option
					System.out.println("Invalid option, please try again.");
				} else {
					// valid option is given, stop looping
					break;				
				}
			}
			
			// find which option user has chosen
			SysModes choise = SysModes.values()[option-1];
			
			// perform task given by user
			switch (choise) {
			case ADD_BLD:
				// add a new building
				addBuilding();
				break;
	
			case SHOW_INFO:
				// ask user for wanted query and perform task
				runQuery();			
				break;
				
			case UPDATE_RSDNT:
				// get apartment's details and update resident according to user
				updateResident();
				break;
				
			case SHUT_DOWN:
				
				// stop running
				keepRunning = false;
				System.out.println("Lights out..");
				try {
					transactor.shutDown();
				} catch (ServerException e) {
					
					// display error message to user
					System.out.println(e.getMessage());
				}
				break;
			}
		
		}
		
	}
}